package camid.services.nodemanager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.reflections.Reflections;

import camid.distribution.activeobject.ObjectScheduler;
import camid.distribution.activeobject.ProxyBuilder;
import camid.distribution.broker.Requestor;
import camid.distribution.events.EventHandler;
import camid.distribution.lifecycle.LifecycleScope;
import camid.distribution.lifecycle.LocationScope;
import camid.distribution.lifecycle.ManagedObject;
import camid.distribution.lifecycle.ObjectManager;
import camid.distribution.protocol.Endpoint;
import camid.services.nameservice.INameService;
import camid.services.nameservice.NameService;
import camid.services.nameservice.NameServiceUtil;
import camid.services.nodemanager.observers.InvokerObserver;
import camid.services.nodemanager.observers.LifecycleObserver;
import camid.services.nodemanager.observers.SystemUsageObserver;
import camid.test.testobjects.Calculator;
import camid.test.testobjects.DelayObject;
import camid.test.testobjects.IntegerBuffer;
import camid.test.testobjects.ServerHelloMessage;
import camid.test.testobjects.StatefulDelayObject;
import camid.util.monitoring.NodeMetrics;

@ManagedObject(lifecycleScope = LifecycleScope.STATIC, locationScope = LocationScope.NODE)
public class NodeManager extends EventHandler implements INodeManager {

	public static final String NODE_MANAGER_OBJECTID = "NodeManager";

	public static final double OBJECT_WEIGHT = 10.0;
	public static final double CPU_WEIGHT = 10.0;
	public static final double RESPONSE_TIME_WEIGHT = 0.001;

	private static final Logger logger = LogManager.getLogger(NodeManager.class
			.getName());

	private INameService nameService;

	private ObjectManager objectManager;

	private String nodeId;

	private Map<String, ObjectStatus> statusMap;

	private Double cpuUsage;

	private Double memoryUsage;

	private Requestor requestor;

	private AtomicInteger alertCounter;

	public NodeManager() {
		super(NODE_MANAGER_OBJECTID);
		this.cpuUsage = 0.0;
		this.memoryUsage = 0.0;
		this.statusMap = Collections
				.synchronizedMap(new HashMap<String, ObjectStatus>());
		this.registerObserver(new LifecycleObserver());
		this.registerObserver(new SystemUsageObserver());
		this.registerObserver(new InvokerObserver());
	}

	public void setRequestor(Requestor requestor) {
		this.requestor = requestor;
	}

	public void setNameService(INameService nameService, int nodePort) {
		this.nameService = nameService;
		this.nodeId = this.nameService.registerNode(NameServiceUtil
				.getLocalEndpoint(nodePort));
	}

	public void setObjectManager(ObjectManager objectManager)
			throws InstantiationException, IllegalAccessException {
		this.objectManager = objectManager;
		this.objectManager.insertObject(nodeId, this);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see camid.services.nodemanager.INodeManager#getNameService()
	 */
	public INameService getNameService() {

		return this.nameService;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#reportObjectCreation(java.lang
	 * .String, java.lang.Class)
	 */
	public void reportObjectCreation(String objectId, Class<?> entityClass) {
		ManagedObject mo = entityClass.getAnnotation(ManagedObject.class);
		if (mo.locationScope().equals(LocationScope.CLOUD)) {
			logger.info("Created object " + entityClass.getCanonicalName()
					+ "/" + nodeId);
			this.nameService.bind(nodeId, objectId,
					entityClass.getCanonicalName());
			this.statusMap.put(objectId, ObjectStatus.ACTIVE);
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#reportObjectDestruction(java.
	 * lang.String)
	 */
	public void reportObjectDestruction(String objectId) {
		this.nameService.unbind(objectId);
		this.statusMap.remove(objectId);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see camid.services.nodemanager.INodeManager#getNodeId()
	 */
	public String getNodeId() {

		return this.nodeId;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#registerDefinition(java.lang.
	 * Class)
	 */
	public void registerDefinition(Class<?> entityClass) {
		logger.info("Registering definition " + entityClass.getCanonicalName());
		this.nameService.registerObjectDefinition(nodeId,
				entityClass.getCanonicalName());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#getEntityData(java.lang.String)
	 */
	public Object getEntityData(String objectId) {
		Object entity = this.objectManager.getObject(objectId).getEntity();
		return entity;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#addEntityData(java.lang.String,
	 * java.lang.Object)
	 */
	public void addEntityData(String objectId, Object entity)
			throws InstantiationException, IllegalAccessException {
		this.objectManager.insertObject(objectId, entity);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see camid.services.nodemanager.INodeManager#getCpuUsage()
	 */
	public Double getCpuUsage() {
		return cpuUsage;
	}

	public void setCpuUsage(Double usage) {
		this.cpuUsage = usage;
	}

	public void setMemoryUsage(Double memory) {
		this.memoryUsage = memory;
	}

	public Double getMemoryUsage() {
		return memoryUsage;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see camid.services.nodemanager.INodeManager#getNumberOfObjects()
	 */
	public Integer getNumberOfObjects() {
		Integer result = 0;
		synchronized (statusMap) {
			for (Entry<String, ObjectStatus> entry : statusMap.entrySet()) {
				if (entry.getValue().equals(ObjectStatus.ACTIVE)) {
					result++;
				}
			}
		}

		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#getObjectStatus(java.lang.String)
	 */
	public ObjectStatus getObjectStatus(String objectId) {
		ObjectStatus status = this.statusMap.get(objectId);
		return status;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#migrateObjects(java.lang.String,
	 * double)
	 */
	public void migrateObjects(String nodeId, Integer quantity) {
		List<String> objectIds = selectObjectsForMigration(quantity);
		Endpoint ep = this.nameService.getNodeEndpoint(nodeId);
		if (requestor != null) {
			ProxyBuilder<INodeManager> builder = new ProxyBuilder<INodeManager>(
					INodeManager.class, UUID.randomUUID().toString(), nodeId,
					NodeManager.class, requestor, ep);
			INodeManager nm = builder.newProxy();
			for (String objectId : objectIds) {
				migrateObject(objectId, nm);
			}
		}
	}

	private List<String> selectObjectsForMigration(Integer quantity) {
		List<String> objectIds = new ArrayList<String>();
		synchronized (statusMap) {
			Iterator<String> hostedIds = statusMap.keySet().iterator();
			while (quantity > 0 && hostedIds.hasNext()) {
				String hostedId = hostedIds.next();
				objectIds.add(hostedId);
				quantity--;
			}
		}
		return objectIds;
	}

	@Override
	public void migrateObject(String objectId, Endpoint nodeEndpoint) {
		if (requestor != null) {
			ProxyBuilder<INodeManager> builder = new ProxyBuilder<INodeManager>(
					INodeManager.class, UUID.randomUUID().toString(), nodeId,
					NodeManager.class, requestor, nodeEndpoint);
			INodeManager nm = builder.newProxy();
			migrateObject(objectId, nm);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * camid.services.nodemanager.INodeManager#migrateObject(java.lang.String,
	 * camid.services.nodemanager.NodeManager)
	 */
	public void migrateObject(String objectId, INodeManager nm) {
		if (this.statusMap.containsKey(objectId)) {
			try {
				this.statusMap.put(objectId, ObjectStatus.MOVING);
				ObjectScheduler scheduler = this.objectManager
						.getObject(objectId);
				Object entity = scheduler.getEntity();
				// if( checkEntityStatefulness(entity) )
				// {
				nm.addEntityData(objectId, entity);
				// }
				this.statusMap.put(objectId, ObjectStatus.MOVED);

			} catch (InstantiationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

	}

	private boolean checkEntityStatefulness(Object entity) {
		boolean result = true;
		if (entity.getClass().isAnnotationPresent(ManagedObject.class)) {
			ManagedObject mo = entity.getClass().getAnnotation(
					ManagedObject.class);
			if (mo.lifecycleScope().equals(LifecycleScope.PER_REQUEST)) {
				result = false;
			}
		}
		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see camid.services.nodemanager.INodeManager#getNodeCost()
	 */
	public Double getNodeCost() {
		Double cost = cpuUsage.doubleValue() * CPU_WEIGHT
				+ this.getNumberOfObjects().doubleValue() * OBJECT_WEIGHT
				+ this.nodeAverageResponseTime * RESPONSE_TIME_WEIGHT;
		logger.info("Cost of " + nodeId + ": " + cost);
		return cost;
	}

	@Override
	public NodeMetrics getNodeMetrics() {
		NodeMetrics metrics = new NodeMetrics();
		metrics.setCpuUsage(cpuUsage);
		metrics.setMemoryUsage(memoryUsage);
		metrics.setNodeThroughput(this.nodeAverageThroughput);
		metrics.setNodeResponseTime(this.nodeAverageResponseTime);
		metrics.setNumberOfObjects(this.getNumberOfObjects());

		logger.info(String.format("Node %s metrics: CPU %f %%; Mem %f %%;"
				+ " RT %f ms; TP %f req/s; Objects %d", nodeId,
				metrics.getCpuUsage(), metrics.getMemoryUsage(),
				metrics.getNodeResponseTime(), metrics.getNodeThroughput(),
				metrics.getNumberOfObjects()));

		return metrics;
	}

	private Double nodeAverageResponseTime = 0.0;

	private Double nodeAverageThroughput = 0.0;

	public void setResponseTime(Double averageTime) {
		this.nodeAverageResponseTime = averageTime;
	}

	public void setThroughput(Double averageThroughput) {
		this.nodeAverageThroughput = averageThroughput;
	}

	public Double getNodeAverageResponseTime() {
		return nodeAverageResponseTime;
	}

	public Double getNodeAverageThroughput() {
		return nodeAverageThroughput;
	}

	public void registerAllManagedObjects() {
		Reflections r = new Reflections("");
		Set<Class<?>> clsSet = r.getTypesAnnotatedWith(ManagedObject.class,
				false);
		if (clsSet.size() > 0) {
			for (Class<?> cls : clsSet) {

				this.registerDefinition(cls);
			}
		} else {
			// class camid.services.nodemanager.NodeManager
			// class camid.test.testobjects.ServerHelloMessage
			// class camid.services.nameservice.NameService
			// class camid.test.testobjects.IntegerBuffer
			// class camid.test.testobjects.Calculator
			// class camid.test.testobjects.DelayObject
			this.registerDefinition(NodeManager.class);
			this.registerDefinition(ServerHelloMessage.class);
			this.registerDefinition(NameService.class);
			this.registerDefinition(IntegerBuffer.class);
			this.registerDefinition(Calculator.class);
			this.registerDefinition(DelayObject.class);
			this.registerDefinition(StatefulDelayObject.class);

		}
	}

	@Override
	public void alert(Integer redirectCount) {
		if(this.alertCounter == null) {
			this.alertCounter = new AtomicInteger(0);
		}
		this.alertCounter.addAndGet(redirectCount);
	}

	@Override
	public Boolean isUnderLoadAlert() {
		if(this.alertCounter == null)
			return null;
		Boolean loadAlert = false;
		int count = this.alertCounter.get();
		if (count > 0) {
			loadAlert = true;
			this.alertCounter.getAndDecrement();
		}
		return loadAlert;
	}

}
